콘텐츠로 이동

07. 논리 함수

7.1 조건 검사

벡터에 조건을 적용하면 원소 개수만큼의 논리 벡터가 만들어집니다. 그런데 if() 조건문처럼 "그래서 결론이 뭔데?"라는 질문에는 길이가 1인 답, 즉 TRUE 또는 FALSE 하나가 필요합니다. 예를 들어 "이 반에 60점 미만인 학생이 한 명이라도 있는가?"와 "이 반 학생 전원이 60점 이상인가?"는 각각 다른 방식으로 논리 벡터를 요약해야 답할 수 있는 질문입니다.

조건을 적용하면 학생별 결과(논리 벡터)까지만 얻을 수 있고, "한 명이라도"·"전원"에 대한 답은 별도로 요약해야 합니다.

scores <- c(85, 55, 90, 40, 70)
scores >= 60
#> [1]  TRUE FALSE  TRUE FALSE  TRUE

any()는 "논리 벡터 중 하나라도 TRUE인가"를, all()은 "논리 벡터 모두가 TRUE인가"를 답하는 함수입니다. 각각 OR과 AND를 벡터 전체로 확장한 것으로 볼 수 있습니다.

any()

any(..., na.rm = FALSE)는 주어진 논리 벡터 중 어느 하나라도 TRUE이면 TRUE를 반환합니다.

인자 - ...: 검사할 논리 벡터입니다. 콤마로 여러 개를 나열할 수 있으며, 이 경우 모두 하나로 이어붙인 것처럼 취급해 그중 하나라도 TRUE가 있는지 검사합니다(...은 여러 값을 한꺼번에 받는 R의 특수 인자로, 자세한 문법은 16.2절에서 다룹니다). - na.rm: 결측치(NA)를 계산에서 제외할지 여부를 지정합니다. 기본값은 FALSE이며, 이 경우 NA는 아래 주의사항처럼 특별하게 처리됩니다. TRUE로 지정하면 NA를 먼저 제거한 뒤 나머지 값만으로 판단합니다.

(x <- seq(-3, 3, by = 1))
#> [1] -3 -2 -1  0  1  2  3

any(x < 0)
#> [1] TRUE

if (any(x < 0)) cat("x는 음수를 포함하고 있습니다\n")
#> x는 음수를 포함하고 있습니다

여러 조건을 한 번에 검사할 수도 있습니다. ...에 조건을 콤마로 나열하면 됩니다.

any(FALSE, FALSE, TRUE)   # 세 값을 이어붙여 검사 -> 하나라도 TRUE면 TRUE
#> [1] TRUE

all()

all(..., na.rm = FALSE)는 주어진 논리 벡터 모두가 TRUE이면 TRUE를 반환합니다. 어느 하나라도 FALSE가 있으면 FALSE를 반환합니다.

인자 - ...: 검사할 논리 벡터입니다. any()와 마찬가지로 여러 개를 콤마로 나열하면 모두 이어붙여 전원 TRUE인지 검사합니다. - na.rm: NA를 제외할지 여부입니다. 기본값 FALSE에서의 동작은 아래 주의사항을 참고하십시오.

(x <- seq(-3, 3, by = 1))
#> [1] -3 -2 -1  0  1  2  3

all(x < 0)
#> [1] FALSE

if (all(x >= -3)) cat("x에 포함된 수는 모두 -3 이상입니다\n")
#> x에 포함된 수는 모두 -3 이상입니다

주의: NA가 섞여 있을 때의 any()·all()

na.rm = FALSE(기본값) 상태에서 any()와 all()은 NA를 "모른다"는 뜻 그대로 취급합니다. 이미 확정된 결과(TRUE 하나 또는 FALSE 하나)가 있으면 NA와 무관하게 그 값을 반환하지만, 확정된 결과가 없는 상태에서 NA만 섞여 있으면 결과도 NA가 됩니다.

any(c(FALSE, NA))                # TRUE가 없고 NA만 있음 -> 확정 불가
any(c(FALSE, NA), na.rm = TRUE)  # na.rm = TRUE: NA를 제거하면 FALSE만 남음 -> 확정
any(c(TRUE, NA))                 # 이미 TRUE가 있으므로 NA와 무관하게 확정
#> [1] NA
#> [1] FALSE
#> [1] TRUE

all(c(TRUE, NA))                 # FALSE가 없고 NA만 있음 -> 확정 불가
all(c(TRUE, NA), na.rm = TRUE)   # na.rm = TRUE: NA를 제거하면 TRUE만 남음 -> 확정
all(c(FALSE, NA))                # 이미 FALSE가 있으므로 NA와 무관하게 확정
#> [1] NA
#> [1] TRUE
#> [1] FALSE

결측치가 섞인 데이터에서는 na.rm 인자를 빠뜨리면 조건문이 예상과 다르게 NA를 반환해 오류로 이어지기 쉬우므로, 결측치 가능성이 있는 데이터에는 na.rm = TRUE를 명시적으로 지정하는 습관을 들이는 것이 안전합니다.

isTRUE() / isFALSE()

isTRUE(x)는 x가 "길이 1의 TRUE"인 경우에만 TRUE를 반환합니다. isFALSE(x)는 반대로 x가 "길이 1의 FALSE"인 경우에만 TRUE를 반환합니다.

인자 - x: 검사할 대상입니다. 논리값뿐 아니라 숫자·문자·벡터·NA·NULL 등 R의 어떤 객체든 넣을 수 있습니다. isTRUE()·isFALSE()는 x가 정확히 "길이 1의 TRUE(또는 FALSE)"가 아니면 오류를 내는 대신 항상 안전하게 FALSE를 반환하도록 설계되어 있습니다.

if() 조건에는 길이 1인 논리값만 들어갈 수 있습니다. 그런데 x == TRUE 같은 비교는 x가 벡터이거나 NA를 포함하면 길이가 1이 아니거나 NA를 반환할 수 있어, 그대로 if()에 넣으면 오류가 나거나 예상 밖의 결과가 나올 위험이 있습니다.

x <- c(TRUE, TRUE)
x == TRUE      # 벡터 그대로 비교되어 길이 2가 됨
#> [1] TRUE TRUE

isTRUE(x)      # 길이가 1이 아니므로 안전하게 FALSE로 판정
#> [1] FALSE

y <- NA
y == TRUE      # NA
#> [1] NA

isTRUE(y)      # NA조차 안전하게 FALSE로 판정
#> [1] FALSE

isTRUE()·isFALSE()는 이처럼 "정확히 TRUE(또는 FALSE) 하나인가"만 판정하고, 그 밖의 모든 경우(벡터, NA, 문자열 "TRUE" 등)에는 안전하게 FALSE를 반환하므로 함수 인자 검증이나 조건문 방어 코드에 즐겨 쓰입니다.

check_option <- function(verbose = FALSE) {
  if (isTRUE(verbose)) cat("자세히 출력합니다\n") else cat("간단히 출력합니다\n")
}
check_option(verbose = "yes")   # 잘못된 값이 들어와도 오류 없이 안전하게 처리됨
#> 간단히 출력합니다

identical() / all.equal()

부동소수점(floating point) 연산은 IEEE 754 표준의 특성상, 수학적으로는 같아야 할 값이 컴퓨터 내부에서는 미세하게 다르게 저장될 수 있습니다. 이 때문에 == 비교가 직관과 다르게 FALSE를 반환하는 일이 자주 벌어집니다.

0.1 + 0.2 == 0.3
#> [1] FALSE

identical()은 두 객체가 값·자료형·속성까지 정확히 일치하는지 엄격하게 검사하고, all.equal()은 부동소수점 오차 수준의 미세한 차이는 "같다"고 봐줍니다.

identical(0.1 + 0.2, 0.3)   # 저장된 이진수 값까지 완전히 같아야 함
#> [1] FALSE

all.equal(0.1 + 0.2, 0.3)   # 오차 허용범위 안이면 TRUE
#> [1] TRUE

인자 - identical(x, y, ...)의 x, y는 비교할 두 객체입니다. 값뿐 아니라 자료형(typeof())과 속성(attributes())까지 하나라도 다르면 FALSE가 됩니다. 이 밖에도 num.eq(숫자 비교 방식), attrib.as.set(속성 순서를 무시할지) 등 비교 기준을 세밀하게 조정하는 인자가 더 있지만, 기본값을 그대로 쓰는 것으로 충분한 경우가 대부분입니다. - all.equal(target, current, tolerance = ..., ...)의 target·current는 비교할 두 값이고, tolerance는 "이 정도 차이는 같다고 봐줄" 허용오차입니다. 기본값은 sqrt(.Machine$double.eps)(약 1.49e-08)로, 부동소수점 계산에서 흔히 생기는 미세한 오차를 넉넉히 덮는 크기입니다. tolerance를 더 작게 지정하면 더 엄격하게, 더 크게 지정하면 더 너그럽게 비교합니다.

identical(x, y)는 자료형이 다르면 값이 같아 보여도 FALSE를 반환할 만큼 엄격합니다. 반면 all.equal()은 값이 다를 경우 TRUE/FALSE가 아니라 차이를 설명하는 문자열을 반환할 수 있다는 점이 특이합니다. 그래서 if() 조건에 바로 쓰면 위험하며, 반드시 isTRUE()로 감싸 "정말로 같다고 판정됐는가"만 확인하는 것이 관례입니다.

identical(1, 1L)             # 실수(double)와 정수(integer)는 자료형이 달라 FALSE
#> [1] FALSE

identical(NA, NA_integer_)   # NA(logical)와 NA_integer_(integer)도 자료형이 달라 FALSE
#> [1] FALSE

all.equal(1, 1.1)            # 값이 다르면 문자열을 반환 -- 논리값이 아님에 주의
#> [1] "Mean relative difference: 0.1"

isTRUE(all.equal(1, 1.1))    # if()에 안전하게 쓰려면 반드시 isTRUE()로 감싸야 함
#> [1] FALSE

허용오차를 직접 조정할 수도 있습니다.

all.equal(1, 1.1, tolerance = 0.2)   # 오차 허용범위를 넉넉하게 지정하면 TRUE
#> [1] TRUE

두 실수(벡터)가 부동소수점 오차를 감안하고도 같은지 판정할 때는 x == y나 identical(x, y)가 아니라 isTRUE(all.equal(x, y)) 관용구를 사용하는 것이 R의 표준적인 방식입니다.

7.2 위치 탐색

논리 벡터에 which()를 적용하면 TRUE인 위치의 인덱스만 뽑아낼 수 있고, 이를 다시 원래 벡터의 인덱싱에 활용할 수 있습니다.

scores <- c(85, 55, 90, 40, 70)
which(scores < 60)          # 조건을 만족하는 위치(인덱스)
#> [1] 2 4

scores[which(scores < 60)]  # 위치를 이용해 실제 값 조회
#> [1] 55 40

which()가 조건을 만족하는 모든 위치를 찾는 일반적인 도구라면, which.min()·which.max()는 "최솟값·최댓값의 위치"라는 자주 쓰이는 특수한 경우를 더 간단하고 빠르게 처리하는 전용 함수입니다.

which()

which(x, arr.ind = FALSE, useNames = TRUE)는 벡터(또는 배열) x에서 TRUE인 것의 위치를 반환합니다. x가 배열인 경우 arr.ind = TRUE이면 배열 형태의 위치를 반환합니다.

인자 - x: 위치를 찾을 논리 벡터 또는 논리형 배열(행렬 포함)입니다. 주의할 점은 which()가 x를 논리형으로 자동 변환해주지 않는다는 것입니다. 대부분의 R 함수와 달리, x에 논리형이 아닌 값(숫자·문자 등)을 그대로 넣으면 오류가 납니다. 그래서 which(1:5)처럼 쓰는 것이 아니라 which(x > 0)처럼 항상 비교·조건 연산의 결과(논리 벡터)를 넣어야 합니다. - arr.ind: x가 배열(또는 행렬)일 때, 결과를 "전체를 한 줄로 늘어놓은 위치"(기본값 FALSE) 대신 "행·열(·층) 좌표" 형태로 받을지 지정합니다. TRUE로 지정하면 각 행이 하나의 원소 좌표를 나타내는 행렬을 반환합니다. - useNames: arr.ind = TRUE이고 x에 dimnames(행 이름·열 이름)가 지정되어 있을 때, 그 이름들을 결과 행렬에도 그대로 붙일지 여부입니다. 기본값 TRUE는 이름을 붙이고, FALSE는 이름 없이 순번([1,], [2,] 등)만 붙입니다. x가 단순 벡터이거나 arr.ind = FALSE인 경우에는 이 인자가 결과에 영향을 주지 않습니다.

# 벡터 위치 반환
(x <- 5:10)
#> [1]  5  6  7  8  9 10

which(x == 7)
#> [1] 3

x[which(x == 7)]
#> [1] 7
# 배열 위치 반환
x <- 5:16
dim(x) <- c(2, 2, 3); x  # x를 2x2 행렬의 3층 배열로 만들기
#> , , 1
#>
#>      [,1] [,2]
#> [1,]    5    7
#> [2,]    6    8
#>
#> , , 2
#>
#>      [,1] [,2]
#> [1,]    9   11
#> [2,]   10   12
#>
#> , , 3
#>
#>      [,1] [,2]
#> [1,]   13   15
#> [2,]   14   16
#>

which(x == 15)                    # 벡터형태의 위치 반환
#> [1] 11

which(x == 15, arr.ind = TRUE)    # 배열형태의 위치 반환
#>      dim1 dim2 dim3
#> [1,]    1    2    3
# 행렬 위치 반환 (배열과 유사)
x <- matrix(5:16, 3, 4); x
#>      [,1] [,2] [,3] [,4]
#> [1,]    5    8   11   14
#> [2,]    6    9   12   15
#> [3,]    7   10   13   16

which(x %% 3 == 0)                   # 벡터형태의 위치 반환
#> [1]  2  5  8 11

which(x %% 3 == 0, arr.ind = TRUE)   # 행렬형태의 위치 반환
#>      row col
#> [1,]   2   1
#> [2,]   2   2
#> [3,]   2   3
#> [4,]   2   4

useNames의 효과는 행렬에 이름(dimnames)이 붙어 있을 때 비로소 드러납니다.

m <- matrix(1:6, 2, 3, dimnames = list(c("r1", "r2"), c("c1", "c2", "c3")))
m
#>    c1 c2 c3
#> r1  1  3  5
#> r2  2  4  6

which(m > 3, arr.ind = TRUE)                    # useNames 기본값 TRUE -> 행 이름이 붙음
#>    row col
#> r2   2   2
#> r1   1   3
#> r2   2   3

which(m > 3, arr.ind = TRUE, useNames = FALSE)  # 이름 없이 순번만 표시
#>      [,1] [,2]
#> [1,]    2    2
#> [2,]    1    3
#> [3,]    2    3
# 데이터 프레임 위치 반환
str(women)   # R 내장 데이터셋 women - 미국 여성 키(인치)와 몸무게(파운드)
#> 'data.frame':    15 obs. of  2 variables:
#>  $ height: num  58 59 60 61 62 63 64 65 66 67 ...
#>  $ weight: num  115 117 120 123 126 129 132 135 139 142 ...

which(women$height < 60)
#> [1] 1 2

women[which(women$height < 60), ]
#>   height weight
#> 1     58    115
#> 2     59    117

which.min()

which.min(x)는 벡터 x의 최솟값 위치를 반환합니다.

인자 - x: 위치를 찾을 숫자형(또는 순서를 비교할 수 있는) 벡터입니다. any()·all()과 달리 na.rm 인자가 따로 없는데, 이는 which.min()·which.max()가 애초에 NA를 항상 무시하고 나머지 값 중에서만 최솟값·최댓값 위치를 찾도록 설계되어 있기 때문입니다.

# 벡터
x <- c(1:4, 0:5, 11); x
#>  [1]  1  2  3  4  0  1  2  3  4  5 11

which.min(x)
#> [1] 5

NA가 섞여 있어도 별도 설정 없이 자동으로 무시됩니다.

which.max(c(1, NA, 5, 3))   # NA는 무시되고, 값 5가 있는 3번째 위치를 반환
#> [1] 3
# 데이터프레임
str(women)
#> 'data.frame':    15 obs. of  2 variables:
#>  $ height: num  58 59 60 61 62 63 64 65 66 67 ...
#>  $ weight: num  115 117 120 123 126 129 132 135 139 142 ...

which.min(women$weight)
#> [1] 1

women[which.min(women$weight), ]
#>   height weight
#> 1     58    115

which.max()

which.max(x)는 벡터 x의 최댓값 위치를 반환합니다. 인자 x의 의미와 NA 처리 방식은 which.min()과 동일합니다.

# 벡터
x <- c(1:4, NA, 11, 0:5); x
#>  [1]  1  2  3  4 NA 11  0  1  2  3  4  5

which.max(x)
#> [1] 6
# 데이터프레임
str(women)
#> 'data.frame':    15 obs. of  2 variables:
#>  $ height: num  58 59 60 61 62 63 64 65 66 67 ...
#>  $ weight: num  115 117 120 123 126 129 132 135 139 142 ...

which.max(women$weight)
#> [1] 15

women[which.max(women$weight), ]
#>    height weight
#> 15     72    164

주의: which.min()·which.max()는 동점(tie)이 여러 개 있어도 가장 먼저 나오는 위치 하나만 반환합니다. 최솟값(또는 최댓값)을 가진 위치를 모두 찾으려면 which(x == min(x))처럼 which()와 min()(또는 max())을 조합해야 합니다.

x <- c(3, 1, 5, 1, 4)
which.min(x)          # 처음 등장한 위치 하나만
which(x == min(x))    # 동점인 위치 모두
#> [1] 2
#> [1] 2 4

7.3 비트 논리 연산 함수

색상 코드(RGB)나 파일 접근 권한(읽기·쓰기·실행)처럼, 여러 개의 켜짐/꺼짐(On/Off) 정보를 정수 하나의 비트(bit) 각각에 압축해서 저장하고 다시 꺼내야 하는 경우가 있습니다.

유닉스 계열 파일 권한은 읽기(4) · 쓰기(2) · 실행(1)을 각각 하나의 비트로 표현하고, 이를 더해 하나의 숫자(예: 6 = 읽기+쓰기)로 압축합니다. 이런 압축과 해제를 R에서 하려면 일반적인 &, | 대신 정수의 비트 단위를 다루는 전용 함수가 필요합니다.

R은 정수형(integer) 값의 각 비트에 대해 AND·OR·XOR·NOT과 자리이동(shift) 연산을 수행하는 bitw*() 계열 함수를 제공합니다. bitwAnd(a, b), bitwOr(a, b), bitwXor(a, b), bitwNot(a), bitwShiftL(a, n), bitwShiftR(a, n)은 모두 정수(또는 정수로 강제 변환 가능한 값)를 입력받아 비트 단위 연산 결과를 정수로 반환합니다.

bitwAnd() / bitwOr() / bitwXor() / bitwNot()

bitwAnd(a, b)는 두 정수의 비트 단위 AND를, bitwOr(a, b)는 비트 단위 OR을, bitwXor(a, b)는 비트 단위 XOR(둘 중 하나만 1일 때 1)을, bitwNot(a)는 비트 단위 NOT(부정)을 반환합니다.

인자 - a, b: 비트 연산을 적용할 정수(integer)입니다. 32비트 정수로 취급되며, 정수가 아닌 실수를 넣으면 자동으로 정수로 변환됩니다. 벡터를 넣으면 원소별로 연산되며, 길이가 다르면 짧은 쪽이 재사용(recycling)됩니다. - bitwNot()은 비교 대상이 없는 단항 연산이라 a 하나만 받습니다.

# 파일 권한 예시: 읽기=4(100), 쓰기=2(010), 실행=1(001)
READ <- 4L; WRITE <- 2L; EXECUTE <- 1L

# 읽기+쓰기 권한을 하나의 정수로 압축
perm <- bitwOr(READ, WRITE)
perm
#> [1] 6
# perm에 특정 권한이 포함되어 있는지 검사 (0이 아니면 포함된 것)
bitwAnd(perm, READ)      # 읽기 권한 포함 여부
#> [1] 4

bitwAnd(perm, EXECUTE)   # 실행 권한 포함 여부
#> [1] 0
bitwXor(5L, 3L)   # 101 XOR 011 = 110
#> [1] 6

a(또는 b)에 벡터를 넣으면 원소마다 연산이 적용됩니다.

bitwAnd(c(1L, 2L, 3L, 4L), 1L)   # 각 값과 1(0001)의 AND -> 최하위 비트(홀짝) 검사
#> [1] 1 0 1 0

주의: bitwNot(a)는 정수를 32비트 부호있는 정수(2의 보수)로 취급해 모든 비트를 뒤집습니다. 따라서 bitwNot(0L)은 직관적으로 예상하기 쉬운 큰 양수가 아니라 -1을 반환합니다.

bitwNot(0L)
#> [1] -1

bitwShiftL() / bitwShiftR()

bitwShiftL(a, n)은 정수 a의 비트를 왼쪽으로 n칸 이동(2를 n번 곱하는 것과 동일)시키고, bitwShiftR(a, n)은 오른쪽으로 n칸 이동(2를 n번 나누는 것과 동일)시킵니다. 플래그 상수를 만들 때 자주 쓰입니다.

인자 - a: 이동시킬 정수입니다. - n: 이동할 비트 칸 수입니다. 0 이상 31 이하의 정수여야 하며, 이 범위를 벗어나면(예: 32 이상, 음수) 오류 없이 조용히 NA를 반환하므로 결과가 뜻밖에 NA로 나온다면 n 값의 범위부터 확인해 보아야 합니다. 또한 bitwShiftL()은 n이 정상 범위에 있더라도 결과가 32비트 signed integer의 표현 범위를 벗어나면 NA를 반환할 수 있습니다(예: bitwShiftL(1L, 31)).

# 2의 거듭제곱(플래그용 상수)을 한 번에 생성
bitwShiftL(1L, 0:3)
#> [1] 1 2 4 8
bitwShiftR(8L, 2L)   # 8을 오른쪽으로 2칸 이동 = 8 / 2^2
#> [1] 2
bitwShiftL(1L, 32L)   # n이 유효 범위(0~31)를 벗어나 NA 반환
#> [1] NA
bitwShiftL(1L, 31L)   # 결과가 32비트 signed integer의 범위를 벗어나서 NA 반환
#> [1] NA